Add feed hero section with carousel, highlights, and ad placement - #6515
Add feed hero section with carousel, highlights, and ad placement#6515tsahimatsliah wants to merge 28 commits into
Conversation
Adds a hero section for the top of the feed, plus a Storybook page to iterate on it: - FeedHeroCarousel: pages through featured posts with the existing featured-wide cards, dot indicators and prev/next chips labelled with the neighbouring headlines. - FeedHeroAd: a compact native/direct ad placement for the rail, built from the existing ad primitives (link, image, attribution, pixel, viewability). - FeedHeroSection: carousel on the left, rail on the right with the ad slot, the Happening Now highlights card and an Explore all CTA. - FeedSectionToolbar: section title with search, filter and bookmark actions for the row above the tag chips. Nothing renders this yet — the layout lives in Storybook under Features/Feed/Hero, composed with the existing ExploreChipsBar and feed cards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The rail placement led with a call-to-action button that dominated a 20rem card, even though the whole card is already a click target. It now carries the same elements as the feed ad card and nothing else: advertiser logo and disclosure, the ad title, matching tags, and the cover image in its own column. Also adds the Storybook coverage needed to review it: responsive breakpoints rendered in per-width iframes, hero section states (no ad, single post, short highlights list), one carousel case per post type, ad placement variants, and toolbar variants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…obile Follow-up on the hero section review: - Drops the "Explore all" button; the highlights card already ends in "Read all". - Flattens the rail: the ad slot and the highlights card lose their background and border, keeping only a hover tint on the ad. - Replaces the two headline-labelled paging buttons with a position counter and two arrow buttons, right-aligned. The headline moves to a tooltip and stays in the button's aria-label. - Renders the featured post through the feed's own list card below laptop, so the top of the mobile feed matches the rest of it. The post-type-to-list-card map moves out of FeedItemComponent into cards/common/listCards.ts so both callers share one map. - Renames the section heading to "Recommended for you" in the story. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The dots on the left already show which post is showing, so the counter next to the arrows was a second readout of the same thing. Shortens the section heading to "Recommended" in the story too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Puts the hero section on the real feed so it can be reviewed with live data. Off by default behind the `feed_hero` flag. - FeedHero fetches the six current `majorHeadlines` for the Happening Now list, then loads the top four of those headlines' posts in full through `feedByIds` for the carousel, keeping the headline order. - The rail ad comes from the feed ad placement, skipped for Plus, and logs impression, viewable impression and click. - Upvote, downvote, bookmark and copy link run the same mutations the feed cards use; post clicks log with a feed origin. - MainFeedLayout mounts it in the feed's own topContent slot on My feed and Popular, and tells the feed to stop inserting its Happening Now card so the headlines are not listed twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
A preview deploy is a production build, so GrowthBook devtools can't force `feed_hero` there and the section stays invisible for review. `?feed_hero=1` turns it on for that browser and remembers it, `?feed_hero=0` turns it back off. To be removed once the flag is configured in GrowthBook — a URL that opts someone into an experiment arm would skew the allocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Review feedback on the live feed: - Caps the section at 80rem and centres it; on a wide monitor it ran the full feed width. - The featured card's cover now fills its column, inset with its own corners like the feed cards, instead of being letterboxed over a blurred backdrop. Gated on `coverImage` so the in-feed wide cards keep the treatment they ship with. - Rebuilds the ad slot at the size of the two promo cards it replaces: a banner cover, then advertiser, title and tags. - Adds a `compact` mode to the highlights content for a surface with no card chrome: smaller heading, tighter rows, and a fade at the bottom of the scroll area so the last row isn't sliced by the pinned footer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
- The ad slot now runs advertiser, title, tags, then the cover, matching the order of the feed cards. - The featured card's summary was clamped at three lines, leaving a gap above the action row on a card that is much taller than a feed card. It runs to six in the hero, behind `expandDescription` so the in-feed wide cards keep the shorter clamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The carousel now rotates on its own. The active indicator fills over the slide's duration and the slide advances on that animation's `animationend`, so pausing the fill pauses the rotation: hovering or focusing anywhere in the hero freezes it mid-count and leaving resumes from the same point instead of restarting. Reduced motion drops the animation, which also stops the rotation. Only a change the reader asked for is announced, so an automatic rotation doesn't talk over a screen reader every few seconds. The paging arrows drop to the tertiary variant so they sit behind the content rather than beside it. In the rail, the ad, the highlight rows and "Read all" now share one text edge on both sides. The ad bleeds past it and is clipped by the column, so its hover surface reads as a full-width band like the rows above it, and the footer link goes flat instead of carrying its own inset pill. The ad's tracking pixel moves out of flow — it was a zero-size flex item still claiming a full gap — which, with a tighter column gap, closes most of the dead space between the ad and Happening Now. The hero and the rail get a little more room between them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…ing Now The slide being replaced now stays mounted on top of the new one until its fade finishes, so the two cross over instead of the card popping. Measured in a browser, the pair never drops below full opacity mid-transition, and the outgoing slide unmounts on its own `animationend` — hence a near-zero duration under reduced motion rather than no animation at all, which would leave it mounted forever. The rail becomes one list: "Happening Now" heads the column and the ad is its first row, sharing the headlines' row treatment with the cover on the right at the cards' ratio. Reclaiming the space the stacked ad took brings the list from three visible headlines to five. The hero's cover grows to two thirds of the card, which leaves the text a third — narrow enough that the headline needed its own size, so `coverImage` and `expandDescription` collapse into one `hero` flag carrying the cover crop, the headline size and the summary clamp together. That split only holds where the text column can still hold a headline: below 1360px the hero falls back to an even split, which on a 1024px laptop is the difference between a full headline and a truncated one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…eadline The cover was taking two thirds of the hero and squeezing the headline, so the wide cards gain a five-column span: the only ratio here where the text needs more than one column, hence a text-span map alongside the image one. Below 1360px the even split still stands. The ad drops into the Happening Now list as the second row rather than the first, so the freshest headline keeps the lead — and as the first row only when there are no headlines to sit under. It now carries a headline's shape: the copy on top, and the disclosure under it in the colour the rows give their timestamps, verified as the same computed value in both themes rather than by eye. The advertiser mark shrinks to the meta line's scale, the cover narrows, and the spacing around both opens up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
… card A five-line headline overran the hero's fixed-height card by about 7px, and `overflow: hidden` took the difference out of the action row, leaving a sliced strip of buttons along the bottom edge. The summary now gives way instead: it is the only shrinkable child, so the headline keeps every line and the actions keep their place. The opt-in is explicit because `base.css` resets everything to `flex-shrink: 0`, which is what let the overflow reach the actions rather than being absorbed higher up. Shrinking alone cut the last line mid-glyph, so the text block reserves a line of bottom padding under a matching fade: at full height the gradient covers only the padding and the last line stays solid, and once the block is squeezed the padding goes first and the cut line fades out. The padding sits on the block rather than the summary because `overflow: hidden` clips at the padding edge, which would leak a seventh line past the six-line clamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The rail gains a full-size direct placement below Happening Now, carrying the feed ad card's own design with its background and border removed so it sits on the page rather than in a box. Its inset goes with the chrome: the creative lines up with the headline rows above it instead of standing 16px inside them. The markup comes out of `AdGrid` into `AdCardContent` so both placements render one design rather than two copies. `AdGrid` keeps the card shell and the auto-rotation, which stays behind: the hero renders inside the feed's `ActiveFeedContext`, so rotating here would refetch against the feed's own ad cache and overwrite a slot the feed still owns. Each slot holds its own query key so the two ask the ad server separately and can come back with different creatives, and each logs its own impression, click and viewability under its own slot name. The rail now runs past the carousel rather than the two columns sharing one height, so the headline list keeps all five rows. The cost is dead space under the carousel, which grows with the card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The hero drops its own width. The feed container already caps this box at the card grid's width, which tracks the reader's column count, so the fixed cap was both too wide at three columns and too narrow at five; without it the section measures exactly the grid beneath it. The rail's hover surfaces were bleeding past their column and being clipped by it, which squared off every corner they had. They now sit inside the column with the padding and the radius the feed cards use, so the shape under the cursor is a rounded card rather than a full-bleed band. The ad card takes the same treatment, and with the chrome back its content keeps the feed card's own inset — which retires the flush option added for the version that had none. More padding costs a row: the headline list shows four now rather than five. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The headline list goes back to being only headlines: the ad that sat in it as a second row is gone, and the five rows it displaced return. The remaining placement is rebuilt to the rail's own measurements rather than the feed cell's, which is what made it read as a transplant: one 16px text edge shared with the headline rows beside it, their row radius on the hover surface, and the disclosure under the copy in the colour those rows give their timestamps. The cover comes down from 160px to 128px. Three of the feed card's controls overran a 270px column by 50px, which put "Remove" outside the card. "Advertise here" goes -- it is the one a reader has least use for, and every ad card in the feed below still carries it -- and the call to action can now shrink, so a long one truncates instead of pushing its neighbour out. The card also takes its own height instead of the column's. Ad copy is short and the cover is deliberately modest, so stretching to the section's height bought nothing but a 190px gap above the action row. With one placement left, the two slots collapse to a single query that the feed underneath can read as well, and the grid stands its own first ad down while the hero is showing one -- shifted rather than skipped, so the creative that slot would have shown moves to the next one instead of being fetched and thrown away. `AdCardContent` goes back into `AdGrid`. It was extracted so both placements could render one design; they are now deliberately different, and the indirection was buying nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
|
The failure is The Running the full shared suite locally on this branch's exact tree: 2598 passed, 1 failed — that one. Re-running the job would fail identically, since the failure is deterministic on the current date rather than flaky, so I have not spent a re-run on it. The fix is one line, and I verified it locally (all 8 tests in the file pass, including the expiry case, which sets its own relative date and is unaffected): - endDate: '2026-09-01T00:00:00.000Z',
+ // Relative so the fixture can't expire on the calendar: a running campaign is
+ // one whose end date is still ahead. The expiry case below sets its own.
+ endDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),I have deliberately not pushed it here — it is unrelated to this PR and belongs in its own change so it can land for everyone rather than waiting on this review. Happy to open that separately if useful. Generated by Claude Code |
…ail rules The placement was built to a 300px column and then shown in one nearly 150px wider, where a 16px headline and a 128px cover read as a widget parked next to two big columns. It now carries the featured card's scale -- the same headline size, a full-size advertiser mark -- and runs the section's height again, with the cover taking whatever the copy leaves rather than sitting at a fixed size. That is what closes the gap the fixed cover opened above the action row last time, and it is the reason the card can be full height without one: 226px of cover at three columns, 286px when the copy is short. The headline rows lose their `border-b`. A bottom border follows the row's corner radius and curves up at both ends, drawing a half-open box around every headline instead of a rule between them. The separator is now drawn inset to the text edge, and the last row does without one. "Read all" regains the bottom inset that lands it on the action row's line, now that there is an action row at that height to meet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
…ard's order The v2 grid sits inset inside the floating card and the hero is its sibling, not its child, so the hero ran 24px wider on both sides than the cards under it and sat flush to the top edge. It now carries the same inset, which moves out of the grid's class list into a constant the two share. Its bottom margin is what is left of the grid's 32px row gap once the grid's own top inset is counted, so the hero stands off the first row the way the rows stand off each other. The ad follows the featured card beside it in order as well as scale: the advertiser mark on top at the size that card gives its source, the headline, its tags, then the disclosure where the card puts its date. The feed's two controls close it. The creative's own call to action is left out -- a third button wrapped the row onto two lines in a 270px column, and the whole card is already the click target, so it was a second route to the same link. Responsive pass over every breakpoint from 320 to 1976: - The headline list's bottom fade only applies from `laptop` now. It exists so the pinned footer doesn't slice a row flat while the list scrolls, but below `laptop` the section stacks, every row fits, and it was dimming the last headline for nothing. - The cover is fixed below `laptop`, where the column runs the full width and letting the creative keep its own aspect put a 600px image on the page. - Whether the ad has a column is one rule in one place now, shared by the hook and the section, so a story can't lay out a fourth track the feed would never render. Storybook gets the product's own breakpoints in the toolbar's device picker, each boundary bracketed by the last width before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Picks up the fix for the expired `usePlusSale` fixture, which had been the one red job on this branch. Two conflicts in FeedItemComponent, both from main renaming or removing what this branch had moved: - main gave the inline list-card map a name, `PostTypeToTagList`. This branch had already lifted the same map into `cards/common/listCards.ts` so the hero and the feed share one, so main's copy goes and the shared one stays. - Live rooms are gone from main -- the cards, the post type, the directory -- and the shared list map still mapped `PostType.LiveRoom`, which no longer exists. Dropped there too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
It was carrying the headline rows' 12px radius, matched to the rail it sits in. It is a card, though, and the cards it sits beside -- the feed's posts and the highlights card, both on `Card` -- round at 16px. The cover keeps 12px, which is what `CardImage` gives the feed card's own. The rows keep theirs: a list row rounds tighter than a card here, the way the highlights card's own rows do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The carousel's dots and arrows ran 12px below "Read all" and the ad's controls. The other two columns each hold a bottom inset that keeps their footers off the section's edge — the rail on its "Read all" wrapper, the ad through its card padding — and the carousel column had none, so its controls went all the way down. Measured after: the dots, both paging arrows, "Read all" with its arrow, and "Advertise here"/"Remove" all centre on 484, and every control box is 32px. The card gives up the 12px, which the summary already knows how to absorb — the long-headline case still clears its action row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
The hero opens with a full-size featured card, and the grid beneath it could open with a second one — two big cards stacked, which reads as a repeat. The existing gate is `startIndex`, which counts items, so at four or five columns the first item is still in the opening row and no value keeps a wide card out of it without also suppressing the ones further down. The placement builder already tracks the row it is on, so the floor is now stated in rows: `minWideCardRow`, defaulting to 0, and set to 1 while the hero is on. Both builders in `useFeed` take it, or the ad cadence and the rendered grid would disagree about the same card. Wide cards resume in the second row and below, with the density cap and the fit-to-row clamp unchanged — a card widening at column 1 of four still takes the three columns left to it, not four. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Changes
Introduces a new hero section component for the feed that displays:
New Components
FeedHero: Container component that fetches headlines and featured posts, manages ad queries and event loggingFeedHeroSection: Layout wrapper combining carousel, highlights, and ad placementFeedHeroCarousel: Post carousel with indicator dots and prev/next navigationFeedHeroAd: Ad card optimized for the rail placementFeedSectionToolbar: Reusable toolbar for feed section headers (search, filters, bookmarks)PostTypeToListCard: Mapping of post types to list card components (mirrors existingPostTypeToWideCard)Integration
MainFeedLayoutfor My Feed and Popular pages whenfeatureFeedHeroflag is enableddisableHighlightCardsprop to prevent duplicate highlights when the hero section displays themFeature Flag
featureFeedHero: Controls hero section visibility on main feed pages (defaults to disabled)Manual Testing
https://claude.ai/code/session_01FWv8BuKdrX1GtDnSobxFAa
Preview domain
https://claude-hero-carousel-layout-e5e0.preview.app.daily.dev